﻿using System;
using System.ComponentModel.Composition;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Red.Scripts.Defines;

namespace Language.Intellisense.Completion
{
    internal class CommandHandler : IOleCommandTarget
    {
        #region Private Data

        private IOleCommandTarget _textViewAdapterCommandHandler;
        private ITextView _textView;
        private ICompletionBroker _broker;
        private ICompletionSession _session;
        private SVsServiceProvider _serviceProvider;
        private Settings _settings;

        #endregion Private Data

        #region C-Tor

        public CommandHandler( IVsTextView textViewAdapter, ITextView textView, ICompletionBroker broker, SVsServiceProvider serviceProvider, Settings settings )
        {
            _textView = textView;
            _broker = broker;
            _serviceProvider = serviceProvider;
            _settings = settings;

            //add the command to the command chain
            textViewAdapter.AddCommandFilter( this, out _textViewAdapterCommandHandler );
        }

        #endregion C-Tor

        #region Private Methods

        private bool TriggerCompletion()
        {
            //the caret must be in a non-projection location 
            SnapshotPoint? caretPoint = _textView.Caret.Position.Point.GetPoint( textBuffer => ( !textBuffer.ContentType.IsOfType( "projection" ) ), PositionAffinity.Predecessor );
            if ( !caretPoint.HasValue )
            {
                return false;
            }

            ITrackingPoint trackingPoint = caretPoint.Value.Snapshot.CreateTrackingPoint( caretPoint.Value.Position, PointTrackingMode.Positive );
            _session = _broker.CreateCompletionSession( _textView, trackingPoint, true );

            //subscribe to the Dismissed event on the session 
            _session.Dismissed += OnSessionDismissed;
            _session.Start();

            return true;
        }

        private bool SwitchCategory( uint commandId )
        {
            if ( _settings[ Setting.IntellisenseTabBehaviour ].Equals( IntellisenseTabBehaviour.Autocomplete ) )
                return false;

            if ( _session != null && commandId == (uint)VSConstants.VSStd2KCmdID.TAB )
            {
                int selectedIndex = _session.CompletionSets.IndexOf( _session.SelectedCompletionSet );

                ++selectedIndex;

                if ( selectedIndex >= _session.CompletionSets.Count )
                {
                    selectedIndex = 0;
                }

                _session.SelectedCompletionSet = _session.CompletionSets[ selectedIndex ];

                return true;
            }

            return false;
        }

        private bool IsCommitChar( char typedChar )
        {
            switch ( typedChar )
            {
                case '.':
                case '(':
                case '[':
                    return true;
            }

            return false;
        }

        private bool CheckForCommit( char typedChar, uint commandId )
        {
            bool isCommitChar = false;

            if
            (
                commandId == (uint)VSConstants.VSStd2KCmdID.RETURN ||
                commandId == (uint)VSConstants.VSStd2KCmdID.TAB ||
                ( _settings[ Setting.AutocompleteOnSpace ].Equals( true ) && char.IsWhiteSpace( typedChar ) ) ||
                ( isCommitChar = IsCommitChar( typedChar ) )
            )
            {
                if ( _session?.IsDismissed == false )
                {
                    _session.Commit();

                    // If the typed character is a '.', commit the completion, but allow a new session to be opened
                    // Since the user is now accessing a member variable
                    if ( !isCommitChar )
                        return true;
                }
            }
            else if ( typedChar == ';' || ( _settings[ Setting.AutocompleteOnSpace ].Equals( false ) && typedChar == ' ' ) )
            {
                _session?.Dismiss();
            }

            return false;
        }

        private bool StartSession()
        {
            bool success = false;

            // If there is not already an active session, create a new one
            if ( _session == null || _session.IsDismissed )
            {
                 TriggerCompletion();
            }

            // If there are no matches, this will dismiss the session
            if( _session?.IsStarted == true )
                _session.Filter();

            return success;
        }

        private bool CheckForSessionStart( Guid commandGroup, VSConstants.VSStd2KCmdID command )
        {
            if ( commandGroup == VSConstants.VSStd2K )
            {
                switch ( command )
                {
                    case VSConstants.VSStd2KCmdID.AUTOCOMPLETE:
                    case VSConstants.VSStd2KCmdID.COMPLETEWORD:
                    case VSConstants.VSStd2KCmdID.SHOWMEMBERLIST:
                        return StartSession();
                }
            }

            return false;
        }

        private bool CheckForSessionStart( char typedChar )
        {
            if ( !typedChar.Equals( char.MinValue ) && ( char.IsLetterOrDigit( typedChar ) || typedChar == '.' ) )
            {
                return StartSession();
            }

            return false;
        }

        private bool Filter( uint commandId )
        {
            switch ( commandId )
            {
                case (uint)VSConstants.VSStd2KCmdID.BACKSPACE:
                case (uint)VSConstants.VSStd2KCmdID.DELETE:

                    if ( _session?.IsDismissed == false )
                    {
                        _session.Filter();
                    }
                    return true;
            }

            return false;
        }

        #endregion Private Methods

        #region Event Handlers

        private void OnSessionDismissed( object sender, EventArgs e )
        {
            _session.Dismissed -= OnSessionDismissed;
            _session = null;
        }

        #endregion Event Handlers

        #region IOleCommandTarget

        int IOleCommandTarget.Exec( ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut )
        {
            if ( VsShellUtilities.IsInAutomationFunction( _serviceProvider ) )
            {
                return _textViewAdapterCommandHandler.Exec( ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut );
            }

            //make a copy of this so we can look at it after forwarding some commands
            uint commandID = nCmdID;
            char typedChar = char.MinValue;

            //make sure the input is a char before getting it
            if ( pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR )
            {
                typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant( pvaIn );
            }

            if ( SwitchCategory( commandID ) )
            {
                return VSConstants.S_OK;
            }

            //check for a commit character
            if ( CheckForCommit( typedChar, commandID ) )
            {
                //also, don't add the character to the buffer
                return VSConstants.S_OK;
            }

            // Checks for things like CTRL+Space
            if ( CheckForSessionStart( pguidCmdGroup, (VSConstants.VSStd2KCmdID)nCmdID ) )
            {
                return VSConstants.S_OK;
            }

            //pass along the command so the char is added to the buffer
            int retVal = _textViewAdapterCommandHandler.Exec( ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut );

            if ( CheckForSessionStart( typedChar ) )
            {
                return VSConstants.S_OK;
            }

            if ( Filter( commandID ) )
            {
                return VSConstants.S_OK;
            }

            return retVal;
        }

        int IOleCommandTarget.QueryStatus( ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText )
        {
            if ( pguidCmdGroup == VSConstants.VSStd2K )
            {
                switch ( (VSConstants.VSStd2KCmdID)prgCmds[ 0 ].cmdID )
                {
                    case VSConstants.VSStd2KCmdID.AUTOCOMPLETE:
                    case VSConstants.VSStd2KCmdID.COMPLETEWORD:
                    case VSConstants.VSStd2KCmdID.SHOWMEMBERLIST:
                        prgCmds[ 0 ].cmdf = (uint)OLECMDF.OLECMDF_ENABLED | (uint)OLECMDF.OLECMDF_SUPPORTED;
                        return VSConstants.S_OK;
                }
            }

            return _textViewAdapterCommandHandler.QueryStatus( ref pguidCmdGroup, cCmds, prgCmds, pCmdText );
        }

        #endregion IOleCommandTarget
    }

    #region Provider

    [Export( typeof( IVsTextViewCreationListener ) )]
    [Name( "token completion handler" )]
    [ContentType( "redscript" )]
    [TextViewRole( PredefinedTextViewRoles.Editable )]
    internal class CommandHandlerProvider : IVsTextViewCreationListener
    {
        #region Properties

        [Import]
        internal IVsEditorAdaptersFactoryService AdapterService = null;
        [Import]
        internal ICompletionBroker CompletionBroker { get; set; }
        [Import]
        internal SVsServiceProvider ServiceProvider { get; set; }
        [Import]
        internal Settings Settings { get; set; }

        #endregion Properties

        #region IVsTextViewCreationListener

        void IVsTextViewCreationListener.VsTextViewCreated( IVsTextView textViewAdapter )
        {
            ITextView textView = AdapterService.GetWpfTextView( textViewAdapter );
            if ( textView == null )
                return;

            Func<CommandHandler> createCommandHandler = delegate () { return new CommandHandler( textViewAdapter, textView, CompletionBroker, ServiceProvider, Settings ); };
            textView.Properties.GetOrCreateSingletonProperty( createCommandHandler );
        }

        #endregion IVsTextViewCreationListener
    }

    #endregion Provider
}
